Skip to content

fix(desktop): pasting an image with Cmd+V works in the Mac app - #817

Open
Ark0N wants to merge 1 commit into
masterfrom
fix/desktop-paste-image
Open

Ark0N wants to merge 1 commit into
masterfrom
fix/desktop-paste-image

Conversation

@Ark0N

@Ark0N Ark0N commented Nov 14, 2026

Copy link
Copy Markdown
Owner

The bug

In Codeman for Mac (packages/desktop, the native app: Rust, tao + wry/WKWebView), pressing ⌘V with a copied picture in a session's terminal did nothing. The same server in a browser uploaded the picture and typed its path into the prompt.

Why

Codeman's ⌘V handler (_handleImagePaste in src/web/public/image-input.js) moves focus to a hidden contenteditable "paste trap", calls document.execCommand('paste'), and then lets the key's own paste land on the trap, where it reads image files out of the paste event. Chromium refuses the execCommand call at once, so only the key's paste happens.

From a WKWebView, WebKit handles that call differently. It opens its DOM paste access menu (a small "Paste" menu at the last mouse-down) and holds the page on a synchronous request until the menu is picked or the clipboard changes. The key's own paste, the one that carries the image, was stuck behind it.

Measured on macOS 27.2 (WebKit 22625) with a throwaway WKWebView test app that runs the same flow:

  • A menu Paste hands the page the image as a File for PNG data, a TIFF+PNG screenshot and a file copied in Finder, so WebKit's clipboard itself is fine.
  • ⌘V through the same trap hung until the next clipboard change.
  • With execCommand('paste') answered false, six ⌘V presses in a row (image data, screenshot, Finder file, text, two repeats) each delivered exactly one paste event to the trap.

The fix

The app already injects a script into every server page (HOST_SHIM in packages/desktop/src/assets.rs). It now wraps Document.prototype.execCommand so that 'paste' returns false, which is what Chromium answers. Every other command reaches WebKit unchanged.

The web page keeps its execCommand('paste') call. src/web/CLAUDE.md and docs/architecture-invariants.md keep it for mobile engines that need it. Fixing it in the app also covers servers on older Codeman versions, since the app is a client of whatever server it connects to.

Tests

  • test/desktop-host-shim-paste.test.ts (new, in the default gate) runs the shim's real text, cut out of assets.rs, in a vm with a stub Document: paste answers false without calling WebKit, other commands pass through with their arguments and this, and a page that is not http(s) is left alone.
  • shim_keeps_its_contract (Rust) pins the override.
  • Rust: cargo fmt --check and clippy with -D warnings are clean (cross-checked for aarch64-apple-darwin). The full cargo test passed on the Mac mini (303 passed, 9 ignored) with the same change on the server-tabs branch (feat(desktop): server tabs, several Codeman servers open in one window #816).
  • A DMG with the fix is with the owner to try on their MacBook.

Changeset included (patch).

Cmd+V with a copied picture in a session's terminal did nothing in the Mac
app, while the same server worked in a browser.

Codeman's Cmd+V handler (_handleImagePaste, src/web/public/image-input.js)
focuses a hidden contenteditable trap and calls
document.execCommand('paste') before the key's own paste lands there.
Chromium refuses that call at once. From a WKWebView, WebKit answers it with
its DOM paste access menu ("Paste", opened at the last mouse-down) and holds
the page on a synchronous request until the menu is picked or the clipboard
changes. The key's own paste, the one that carries the image, was stuck
behind it.

Measured on macOS 27.2 (WebKit 22625) with a throwaway WKWebView probe:
- a menu Paste hands the page the image as a File for PNG data, a
  TIFF+PNG screenshot and a file copied in Finder, so WebKit itself is fine;
- Cmd+V through the same trap hung until the next clipboard change;
- with execCommand('paste') answered false, six Cmd+V presses in a row
  (image data, screenshot, Finder file, text, two repeats) each delivered
  exactly one paste event to the trap.

The fix lives in the app's HOST_SHIM, which every server page already gets:
it wraps Document.prototype.execCommand so 'paste' returns false the way
Chromium answers it, and every other command reaches WebKit unchanged. The
web page keeps its execCommand call: src/web/CLAUDE.md keeps it for mobile
engines that need it, and doing it in the app also covers servers on older
Codeman versions.

test/desktop-host-shim-paste.test.ts runs the shim's real text, cut out of
assets.rs, in a vm with a stub Document. shim_keeps_its_contract pins the
override too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants